import backtrader as bt  
import pandas as pd
from datetime import datetime
import matplotlib.pyplot as plt

from backtrader_plotting import Bokeh
from backtrader_plotting.schemes import Tradimo

# 读取csv
data_stock = pd.read_csv('法宝回测/data/sh.csv') # 股票日数据
data_signals = pd.read_csv('法宝回测/data/bwwmacd.csv') # 股票指标数据(大智慧导出)

# 提取需要列
selected_columns = ['日期', '开盘', '最高', '最低', '收盘', '成交量', 'MA5', 'MA10', 'MA30', 'MA60']
data_stock = data_stock[selected_columns]

# 改变列名称
data_stock.columns = ['date', 'open', 'high', 'low', 'close', 'volume','MA5','MA10','MA30',	'MA60']
data_signals.columns = ['date', 'M5', 'M10', 'M20']

# 日期列转换时间类型
data_stock['date'] = pd.to_datetime(data_stock['date'])
data_signals['date'] = pd.to_datetime(data_signals['date'])

# 时间索引
data_stock.index = pd.to_datetime(data_stock['date'])
data_signals.index = pd.to_datetime(data_signals['date'])

# backtrader投喂数据格式
data_stock = data_stock.iloc[:, :6]
data_stock['openinterest'] = 0

# 初始化一个新的列来存储金叉死叉信号  
data_signals['signal'] = 1 
#================================================================================  
# 使用shift函数来访问前一行的值，然后设置新的信号列  
# 注意这里我们使用了向量化操作，而不是循环  
data_signals.loc[(data_signals['M5'] > data_signals['M10'].shift(1)) &   
               (data_signals['M5'].shift(1) < data_signals['M10'].shift(2)),   
               'signal'] = 2 
  
data_signals.loc[(data_signals['M5'] < data_signals['M10'].shift(1)) &   
               (data_signals['M5'].shift(1) > data_signals['M10'].shift(2)),   
               'signal'] =0 
  
# 注意：这里我们使用了.shift(1)来访问前一行（即上一个交易日）的'M5'值  
# 因为我们想要比较当前行的'M5'与前一行的'M5'  
data_signals = data_signals[['date', 'signal']]
#==============================================================================

# 自定义数据源，这里仅展示框架，具体实现需要根据你的CSV文件格式进行调整  
class MyStockData(bt.feeds.PandasData):  # MyStockData 类定义了如何从Pandas DataFrame中提取股票数据。
    # params 元组指定了日期、开盘价、最高价、最低价、收盘价和交易量（如果有的话）在DataFrame中的列索引。
    params = (  
        ('datetime', 0),  # 日期在DataFrame中的列索引  
        ('open', 1),      # 开盘价在DataFrame中的列索引  
        ('high', 2),      # 最高价在DataFrame中的列索引  
        ('low', 3),       # 最低价在DataFrame中的列索引  
        ('close', 4),     # 收盘价在DataFrame中的列索引  
        ('volume', 5),    # 交易量在DataFrame中的列索引（如果有的话）  
        ('openinterest', -1),    # 交易量在DataFrame中的列索引（如果有的话）  
    )  
  
class MySignalData(bt.feeds.PandasData):  # MySignalData 类用于处理信号数据。
    lines = ('signal',)  # lines = ('signal',) 是一个元组的示例，
    # 指定了日期和信号在DataFrame中的列索引。
    params = (  
        ('datetime', 0),  # 日期在DataFrame中的列索引  
        ('signal', 1),    # 信号在DataFrame中的列索引，假设1为买入，-1为卖出，0为不操作  
    )  
#===============================================================================
class MyStrategy(bt.Strategy):
    params = (
       
    )

    def __init__(self):
        self.order = None
        self.dataclose = self.datas[0].close  
        self.datasignal = self.datas[1].signal  
        pass

    def downcast(self, amount, lot):
	#使用整数除法 amount // lot 来找到 amount 可以被 lot 整除的最大次数。
        return abs(amount // lot * lot) 
        """ 	   
		self 的出现意味着这个函数实际上是类的一个方法，而不是一个独立的函数。
		class MyClass:  
 		   def downcast(self, amount, lot):  
  		      return abs(amount // lot * lot)  
  
		# 使用方法  
		my_instance = MyClass()  
		result = my_instance.downcast(123, 10)  
		print(result)  # 输出 120
	    """

    # 可以不要，但如果你数据未对齐，需要在这里检验
    # def prenext(self):
    #     print('prenext 执行 ', self.datetime.date(), self.getdatabyname('300015')._name
    #           , self.getdatabyname('300015').close[0])
    #     pass

    def next(self):
        # 检查是否有指令执行，如果有则不执行这bar
        if self.order:
            return
        # 回测如果是最后一天，则不进行买卖
        if pd.Timestamp(self.data.datetime.date(0)) == end_date:
            return
        if not self.position:  # 没有持仓
            # 执行买入条件判断：收盘价格上涨突破20日均线；
            # 不要在股票剔除日前一天进行买入
            if self.datasignal[0] == 2:  # 如果信号为1，买入  
                # 永远不要满仓买入某只股票
                order_value = self.broker.getvalue() * 0.98
                order_amount = self.downcast(order_value / self.dataclose[0], 100)
                self.order = self.buy(self.datas[0], order_amount, name=self.datas[0]._name)

        else:
            # 执行卖出条件判断：收盘价格跌破20日均线，或者股票剔除
            if self.datasignal[0] == 0:
                # 执行卖出
                self.order = self.order_target_percent(self.datas[0], 0, name=self.datas[0]._name)
                self.log(f'卖{self.datas[0]._name},price:{self.dataclose[0]:.2f},pct: 0')
        pass

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            if order.isbuy():
                self.log(
                    f"买入{order.info['name']}, 成交量{order.executed.size}，成交价{order.executed.price:.2f} 订单状态：{order.status}")
                self.log('买入后当前资产：%.2f 元' % self.broker.getvalue())
            elif order.issell():
                self.log(
                    f"卖出{order.info['name']}, 成交量{order.executed.size}，成交价{order.executed.price:.2f} 订单状态：{order.status}")
                self.log('卖出后当前资产：%.2f 元' % self.broker.getvalue())
            self.bar_executed = len(self)

        # Write down: no pending order
        self.order = None

    def log(self, txt, dt=None):
        """
        输出日期
        :param txt:
        :param dt:
        :return:
        """
        dt = dt or self.datetime.date(0)  # 现在的日期
        print('%s , %s' % (dt.isoformat(), txt))

    pass

    def notify_trade(self, trade):
        '''可选，打印交易信息'''
        pass

#===============================================================================
# 创建Cerebro引擎  
cerebro = bt.Cerebro()  

# # 设置回测时间范围
start_date = datetime(2000, 10, 5)
end_date = datetime(2024, 6, 7)
  
# 添加数据源  
datafeed_stock = MyStockData(dataname=data_stock, fromdate=start_date, todate=end_date)  
datafeed_signal = MySignalData(dataname=data_signals, fromdate=start_date, todate=end_date)  

# 将数据源添加到cerebro  
cerebro.adddata(datafeed_stock, name="000001")  
cerebro.adddata(datafeed_signal, name="zb")  
  
# 添加策略  
cerebro.addstrategy(MyStrategy)  
  
# 设置初始资金和手续费
start_cash = 1000000
cerebro.broker.setcash(start_cash)
#cerebro.broker.setcommission(commission=0.002)

# 添加策略分析指标
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='tradeanalyzer')  
cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='annualReturn')  
cerebro.addanalyzer(bt.analyzers.Returns, _name='annualizedReturns', tann=252)  # 使用 'annualizedReturns' 代替 '_Returns'  
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')  
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpeRatio')  # 使用 'sharpeRatio' 代替 'sharpe'  
cerebro.addanalyzer(bt.analyzers.Returns, _name='totalReturns')  # 使用 'totalReturns' 代替重复的 'returns'  
# 假设 'TimeReturn' 是有效的分析器  
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='timeReturn')  # 假设这是有效的
  
# 运行回测  
results=cerebro.run()  

# 输出结果  
#获取回测结果并打印
port_value = cerebro.broker.getvalue()
pnl = port_value - start_cash
print(f"初始资金: {start_cash}\n回测期间：{start_date.strftime('%Y%m%d')}:{end_date.strftime('%Y%m%d')}")
print(f"总资金: {round(port_value, 2)}")
print(f"净收益: {round(pnl, 2)}")#计算胜率
total_trades = results[0].analyzers.tradeanalyzer.get_analysis()['total']['total']
won_trades = results[0].analyzers.tradeanalyzer.get_analysis()['won']['total']
win_rate = (won_trades / total_trades) * 100 if total_trades > 0 else 0
print('总交易次数:', total_trades)
print('盈利次数:', won_trades)
print('胜率%:', win_rate)
# 打印分析器输出结果
# print(f"初始资金: {start_cash}\n回测期间:{start_date.strftime('%Y-%m-%d')} : {end_date.strftime('%Y-%m-%d')}")
print('年度汇报:', results[0].analyzers.annualReturn.get_analysis())
print('年化收益%:', results[0].analyzers.annualizedReturns.get_analysis()['rnorm100'])
print('最大回撤比例%:', results[0].analyzers.drawdown.get_analysis().max.drawdown)
print('夏普比率:', results[0].analyzers.sharpeRatio.get_analysis()['sharperatio'])
print('累计收益：', results[0].analyzers.totalReturns.get_analysis()['rtot'])

print(f'最后投资金额：{round(cerebro.broker.getvalue(), 2)}')

plotconfig = {
    'id:ind#0': dict(
        subplot=True,
    ),
}
b = Bokeh(style='line', scheme=Tradimo(),plotconfig=plotconfig)
cerebro.plot(b)